<?php
/**
 * @link https://www.kancloud.cn/cleverstone/ymb2
 * @copyright Copyright (c) 2020 Yii Manager Software LLC
 */

namespace builder\export;

use builder\base\Builder;
use builder\base\ViewBuilderInterface;
use builder\helper\App;
use builder\helper\H5;
use builder\helper\RegEx;
use yii\base\InvalidArgumentException;
use yii\data\Pagination;
use yii\web\AssetBundle;
use yii\web\View;

/**
 * xls导出构建器
 *  -------------------- HTML选项 --------------------
 * @property bool $partial 是否是局部继承，默认false
 * @property string $title 页面标题
 * @property array $extraJs 设置额外的Js
 * @property array $extraCss 设置额外的Css
 * @property array $extraAssetBundle 设置额外的静态资源包
 * @property int $totalCount 导出数据总数
 * @property int $limit 每页导出数据条数
 * @property array $route ajax路由
 *  -------------------- AJAX选项 --------------------
 * @property array $query 查询实例或数据
 * @property-read array $header 导出列标题
 * @property array $columns 导出列数据
 * @property array $columnDependency 导出列依赖
 * @author clevstn
 * @since ym2.0
 */
class ExportBuilder extends Builder implements ViewBuilderInterface
{
    // 头部切点
    const POS_HEAD = 0x01;

    // 尾部切点
    const POS_END = 0x02;

    /**
     * @var array 切点
     * @see registerPointcut()
     * @see beginPage()
     * @see endPage()
     */
    private $_pointcutBody = [];

    /**
     * @var bool 是否局部继承
     * @see $partial
     * @see setPartial()
     * @see getPartial()
     */
    private $_partial = false;

    /**
     * @var string 标题
     * @see $title
     * @see setTitle()
     * @see getTitle()
     */
    private $_title = '下载文件';

    /**
     * @var array 额外的JS
     * ```php
     * $export->extraJs = <<<JS
     *      console.log("测试一下 [setExtraCss] 方法;")
     * JS;
     * ```
     * @see $extraJs
     * @see setExtraJs()
     * @see getExtraJs()
     */
    private $_extraJs = [];

    /**
     * @var array 额外的CSS
     * ```php
     * $export->extraCss = <<<CSS
     *      body{background-color: #fff;}
     * CSS;
     * ```
     * @see $extraCss
     * @see setExtraCss()
     * @see getExtraCss()
     */
    private $_extraCss = [];

    /**
     * @var array 额外的静态资源包
     * ```php
     * $export->extraAssetBundle = ExampleAsset::class;
     * ```
     * @see $extraAssetBundle
     * @see setExtraAssetBundle()
     * @see getExtraAssetBundle()
     */
    private $_extraAssetBundle = [];

    /**
     * @var int 导出数据总数
     * @see $totalCount
     * @see setTotalCount()
     * @see getTotalCount()
     */
    private $_totalCount = 0;

    /**
     * @var int 导出每页数据条数
     * @see $limit
     * @see setLimit()
     * @see getLimit()
     */
    private $_limit = 5000;

    /**
     * @var array|null ajax请求URL
     * @see $route
     * @see setRoute()
     * @see getRoute()
     */
    private $_route = [];

    /**
     * @var array 查询实例或数据
     * @see $query
     * @see setQuery()
     * @see getQuery()
     */
    private $_query = [];

    /**
     * @var array 导出列标题
     * @see $header
     * @see setColumns()
     * @see getHeader()
     */
    private $_header = [];

    /**
     * @var array 导出列字段
     * @see $columns
     * @see setColumns()
     * @see getColumns()
     */
    private $_columns = [];

    /**
     * @var array|callable 导出列依赖
     * @see $columnDependency
     * @see setColumnDependency()
     * @see getColumnDependency()
     */
    private $_columnDependency = [];

    /**
     * 静态实例化
     * @param array $config
     * @return $this
     */
    public static function instance($config = [])
    {
        return new static($config);
    }

    /**
     * 注册切点
     * @param string $content 注册的内容
     * @param int $pos 切点标记
     * @return $this
     */
    public function registerPointcut($content, $pos = self::POS_HEAD)
    {
        $this->_pointcutBody[$pos][] = $content;
        return $this;
    }

    /**
     * 页面头部位置
     * @return null|string
     */
    public function beginPage()
    {
        if (empty($this->_pointcutBody[self::POS_HEAD])) {
            return null;
        }

        return implode("\n", $this->_pointcutBody[self::POS_HEAD]);
    }

    /**
     * 页面尾部位置
     * @return null|string
     */
    public function endPage()
    {
        if (empty($this->_pointcutBody[self::POS_END])) {
            return null;
        }

        return implode("\n", $this->_pointcutBody[self::POS_END]);
    }

    /**
     * 设置视图局部继承
     * @param bool $boolean
     * @return $this
     */
    public function setPartial($boolean = true)
    {
        $this->_partial = $boolean;
        return $this;
    }

    /**
     * @return bool
     */
    public function getPartial()
    {
        return $this->_partial;
    }

    /**
     * 设置标题
     * @param string $title 标题
     * @return $this
     */
    public function setTitle($title)
    {
        $this->_title = $title;
        return $this;
    }

    /**
     * @return string
     */
    public function getTitle()
    {
        return $this->_title;
    }

    /**
     * 设置额外的JS
     * @param array|string $js 额外的JS脚本
     * @return $this
     */
    public function setExtraJs($js)
    {
        $js = (array)$js;
        foreach ($js as &$value) {
            if (is_callable($value)) {
                $value = call_user_func($value);
            }
        }

        $this->_extraJs = array_merge($this->_extraJs, $js);
        return $this;
    }

    /**
     * 获取额外的JS
     * @return array
     */
    public function getExtraJs()
    {
        return $this->_extraJs;
    }

    /**
     * 设置额外的CSS
     * @param array|string $css 额外的CSS脚本
     * @return $this
     */
    public function setExtraCss($css)
    {
        $css = (array)$css;
        foreach ($css as &$value) {
            if (is_callable($value)) {
                $value = call_user_func($value);
            }
        }

        $this->_extraCss = array_merge($this->_extraCss, $css);
        return $this;
    }

    /**
     * 获取额外的CSS
     * @return array
     */
    public function getExtraCss()
    {
        return $this->_extraCss;
    }

    /**
     * 设置额外的静态资源包
     * @param string|array $extraAssetBundle 静态资源包
     * @return $this
     */
    public function setExtraAssetBundle($extraAssetBundle)
    {
        $extraAssetBundle = (array)$extraAssetBundle;

        // 检查
        foreach ($extraAssetBundle as $assetBundle) {
            $tempObj = new $assetBundle;
            if (!$tempObj instanceof AssetBundle) {
                throw new InvalidArgumentException($assetBundle . ' is not instance of ' . AssetBundle::class);
            }
        }

        $this->_extraAssetBundle = array_merge($this->_extraAssetBundle, $extraAssetBundle);
        return $this;
    }

    /**
     * 获取额外的静态资源包
     * @return array
     */
    public function getExtraAssetBundle()
    {
        return $this->_extraAssetBundle;
    }

    /**
     * 是否自动发起数据请求
     * @param int|callable $count
     * @return $this
     */
    public function setTotalCount($count)
    {
        if (is_callable($count)) {
            $count = call_user_func($count);
        }

        $this->throwExceptionIfNotNumeric($count);
        $this->_totalCount = $count;

        return $this;
    }

    /**
     * @return int
     */
    public function getTotalCount()
    {
        return $this->_totalCount;
    }

    /**
     * 设置导出每页数据条数
     * @param int $rowCount
     * @return $this
     */
    public function setLimit(int $rowCount)
    {
        $this->_limit = $rowCount;
        return $this;
    }

    /**
     * @return int
     */
    public function getLimit()
    {
        return $this->_limit;
    }

    /**
     * 设置ajax路由
     * @param string|array $route
     * @return $this
     */
    public function setRoute($route)
    {
        $this->_route = (array)$route;
        return $this;
    }

    /**
     * @return array
     */
    public function getRoute()
    {
        return $this->_route;
    }

    /**
     * 设置查询的
     * ```php
     * // 可回调函数
     * $export->setQuery(function () {
     *      for ($i = 0; $i < 500; $i++) {
     *          $data[] = ['id' => $i + 1, 'name' => Str::random(5), 'age' => rand(16, 50), 'class' => 'Grade ' . ($i + 1), 'county' => 'China', 'sex' => range(1, 2)];
     *      }
     *      return $data;
     * });
     * // 数组
     * $export->setQuery([
     *      ['a' => 1, 'b' => 2, 'c' => 3],
     *      ['a' => 4, 'b' => 5, 'c' => 6],
     *      ['a' => 7, 'b' => 8, 'c' => 9],
     * ]);
     * ```
     * @param array|callable $query
     * @return $this
     */
    public function setQuery($query)
    {
        if (is_callable($query)) {
            $query = call_user_func($query);
        }

        $this->throwExceptionIfNotArray($query);
        $this->_query = $query;

        return $this;
    }

    /**
     * @return array
     */
    public function getQuery()
    {
        return $this->_query;
    }

    /**
     * 设置导出列
     * ```php
     * $builder->setColumns([
     *      'ID' => 'id',
     *      '姓名' => 'name',
     *      '年龄' => 'age',
     *      '班级' => 'grade',
     *      '国家' => 'county',
     *      '性别' => function ($row, $columnDependency) {
     *          return $row['sex'] == 1 ? '男' : '女';
     *      }
     * ]);
     * ```
     * @param array $columns
     * @return $this
     */
    public function setColumns(array $columns)
    {
        $this->_header = array_keys($columns);
        $this->_columns = array_values($columns);

        return $this;
    }

    /**
     * @return array
     */
    public function getHeader()
    {
        return $this->_header;
    }

    /**
     * @return array
     */
    public function getColumns()
    {
        static $columns;
        if (isset($columns)) return $columns;

        $query = $this->query;
        $columnFields = $this->_columns;
        $temp = [];

        foreach ($query as $i => $value) {
            $temp[$i] = [];
            foreach ($columnFields as $field) {
                $temp[$i][] = is_callable($field) ? call_user_func($field, $value, $this->columnDependency) : (!empty($value[$field]) ? $value[$field] : '-');
            }
        }

        $columns = $temp;
        return $columns;
    }

    /**
     * 设置导出列依赖
     * ```php
     * $builder->setColumnDependency([
     *      'dep1' => ['a' => 1, 'b' => 2],
     *      'dep2' => ['c' => 1, 'd' => 2],
     * ]);
     * ```
     * @param array|callable $columnDependency
     * @return $this
     */
    public function setColumnDependency($columnDependency)
    {
        $this->_columnDependency = $columnDependency;
        return $this;
    }

    /**
     * @return array
     */
    public function getColumnDependency()
    {
        static $columnDependency;
        if (isset($columnDependency)) return $columnDependency;

        if (is_callable($this->_columnDependency)) {
            $temp = call_user_func($this->_columnDependency, $this->query);
        } else {
            $temp = $this->_columnDependency;
        }

        $this->throwExceptionIfNotArray($temp);
        $columnDependency = $temp;

        return $columnDependency;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'export';
    }

    /**
     * {@inheritdoc}
     */
    public function getViewPath()
    {
        return __DIR__;
    }

    /**
     * 内容输出
     * @param bool $onlyRenderContent 是否仅渲染内容
     * @return \yii\web\Response|string
     * @throws \yii\base\InvalidConfigException
     */
    public function render($onlyRenderContent = false)
    {
        if ($this->isAjax) {
            return $this->_renderAjax();
        } else {
            return $this->_renderHtml($onlyRenderContent);
        }
    }

    /**
     * AJAX输出
     * @return \yii\web\Response
     */
    public function _renderAjax()
    {
        return $this->asOk('success', [
            'header' => $this->header,
            'columns' => $this->columns,
        ]);
    }

    /**
     * 渲染HTML
     * @param bool $onlyRenderContent 是否仅渲染内容
     * @return string
     * @throws \yii\base\InvalidConfigException
     */
    private function _renderHtml($onlyRenderContent = false)
    {
        $pageLinker = $this->_pageLinker();

        // 设置页面TITLE
        $this->_view->title = H5::encode(App::params('admin.brandName') . ' | ' . $this->title);

        // 注册JS脚本
        $this->_view->registerJs($this->_resolveJsScript([
            'linker' => $pageLinker,

        ]), View::POS_END);

        // 注册额外CSS脚本
        if (!empty($this->extraCss)) {
            $this->_view->registerCss(implode("\n", $this->extraCss));
        }

        // 注册额外的AssetBundle
        if (!empty($this->extraAssetBundle)) {
            foreach ($this->extraAssetBundle as $assetBundle) {
                $this->_view->registerAssetBundle($assetBundle);
            }
        }

        // 渲染表单内容
        $content = $this->_view->render('view', [
            'linker' => $pageLinker,

        ], $this);

        // 仅渲染内容
        if ($onlyRenderContent) {
            return $content;
        }

        // 局部布局渲染(不包含: 菜单和导航栏)
        if ($this->partial) {
            return $this->_view->renderFile($this->partialPath, ['content' => $content], $this);
        }

        // 主布局渲染(包含: 菜单和导航栏)
        return $this->_view->renderFile($this->mainPath, ['content' => $content], $this);
    }

    /**
     * 生成下载链接
     * @return array
     */
    private function _pageLinker()
    {
        $routeArray = $this->route;
        $routeStr = $routeArray[0] ?? null;
        unset($routeArray[0]);
        $params = array_replace_recursive($this->get, $routeArray);
        unset($params['page'], $params['limit']);

        $pagination = new Pagination([
            'pageParam' => 'page',
            'pageSizeParam' => 'limit',
            'pageSizeLimit' => false,
            'totalCount' => $this->totalCount,
            'defaultPageSize' => $this->limit,
            'route' => $routeStr,
            'params' => $params,
        ]);
        $pageCount = $pagination->pageCount;
        $links = [];
        for ($page = $pagination->page; $page < $pageCount; $page++) {
           $links[] = $pagination->createUrl($page);
        }

        return $links;
    }

    /**
     * 解析JS脚本
     * @param array $renderParams 导入变量
     * @return string
     */
    private function _resolveJsScript($renderParams)
    {
        $jsScripts = $this->_view->render('app', $renderParams, $this);

        return RegEx::js($jsScripts);
    }
}